package com.google.code.magja.soap;

import java.util.List;
import java.util.Map;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;

public class SoapCallFactory {

	private OMFactory fac;

	private OMNamespace noNs;

	private OMNamespace mag;

	private OMNamespace soapEnc;

	private OMNamespace soapXml;

	private OMNamespace xsi;

	private OMNamespace xsd;

	private static final String CORE_LOGIN = "login";

	private static final String CORE_CALL = "call";

	private static final String CORE_MULTI_CALL = "multiCall";

	private static final String CORE_LOGOUT = "endSession";

	private static final String SESSION_ID = "sessionId";

	private static final String RESOURCE_PATH = "resourcePath";

	private static final String ARGUMENTS = "args";

	private static final String MULTI_CALLS = "calls";

	private static final String MULTI_CALL_OPTIONS = "options";

	public SoapCallFactory() {
		fac = OMAbstractFactory.getOMFactory();

		// Blank namespace for factory methods which require a namespace as
		// argument
		// but no namespace markers should be added to attributes or tags
		noNs = fac.createOMNamespace("", "");

		// Magento namespace
		mag = fac.createOMNamespace("urn:Magento", "mag");

		// General namespaces, needed for soap
		xsi = fac.createOMNamespace(
				"http://www.w3.org/2001/XMLSchema-instance", "xsi");
		xsd = fac.createOMNamespace("http://www.w3.org/2001/XMLSchema", "xsd");
		soapXml = fac.createOMNamespace("http://xml.apache.org/xml-soap",
				"SOAP-XML");
		soapEnc = fac.createOMNamespace(
				"http://schemas.xmlsoap.org/soap/encoding/", "SOAP-ENC");
	}

	/**
	 * Creates a Soap method for login
	 * 
	 * @param user
	 * @param password
	 * @return the created method as axiom element
	 */
	public OMElement createLoginCall(String user, String password) {
		OMElement method = fac.createOMElement(CORE_LOGIN, mag);

		OMElement paramUser = fac.createOMElement("username", mag);
		paramUser.addChild(fac.createOMText(paramUser, user));
		method.addChild(paramUser);

		OMElement paramKey = fac.createOMElement("apiKey", mag);
		paramKey.addChild(fac.createOMText(paramKey, password));
		method.addChild(paramKey);
		return method;
	}

	/**
	 * Creates a soap method to end a session
	 * 
	 * @param sessionId
	 * @return the created method as axiom element
	 */
	public OMElement createLogoutCall(String sessionId) {
		OMElement method = fac.createOMElement(CORE_LOGOUT, mag);
		OMElement paramSession = fac.createOMElement(SESSION_ID, mag);
		paramSession.addChild(fac.createOMText(paramSession, sessionId));
		method.addChild(paramSession);
		return method;
	}

	@SuppressWarnings("unchecked")
	public OMElement createCall(String sessionId, String methodPath, Object arg) {
		OMElement method = fac.createOMElement(CORE_CALL, mag);

		// Register XML namespaces in method
		method.declareNamespace(soapEnc);
		method.declareNamespace(soapXml);
		method.declareNamespace(xsi);
		method.declareNamespace(xsd);

		OMElement paramSession = fac.createOMElement(SESSION_ID, noNs);
		paramSession.addChild(fac.createOMText(paramSession, sessionId));
		method.addChild(paramSession);

		OMElement resourcePath = fac.createOMElement(RESOURCE_PATH, noNs);
		resourcePath.addChild(fac.createOMText(resourcePath, methodPath));
		method.addChild(resourcePath);

		OMElement paramArgs;
		if (arg instanceof List) {
			List<Object> args = (List<Object>) arg;
			paramArgs = fac.createOMElement(ARGUMENTS, noNs);
			paramArgs.addAttribute("arrayType", xsd.getPrefix() + ":ur-type["
					+ args.size() + "]", soapEnc);
			paramArgs.addAttribute("type", soapEnc.getPrefix() + ":Array", xsi);

			for (Object argument : args) {
				paramArgs.addChild(typedElement(noNs, "item", argument));
			}
		} else {
			paramArgs = this.typedElement(noNs, ARGUMENTS, arg);
		}
		// paramArgs.addChild(itemArg);
		method.addChild(paramArgs);

		return method;
	}

	/**
	 * Dispatcher function which decides, how the axiom subtree for a value is
	 * created.
	 * 
	 * @param elementNs
	 * @param name
	 * @param value
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private OMElement typedElement(OMNamespace elementNs, String name,
			Object value) {
		if (value instanceof String) {
			/*
			 * Simple key-value map <item><key
			 * xsi:type="xsd:string">name</key><value
			 * xsi:type="xsd:string">value</value></item>
			 */
			return this.typedElement(elementNs, name, (String) value,
					xsd.getPrefix() + ":string");
		} else if (value instanceof Integer) {
			/*
			 * Simple key-value map <item><key
			 * xsi:type="xsd:string">name</key><value
			 * xsi:type="xsd:int">value</value></item>
			 */
			return this.typedElement(elementNs, name,
					((Integer) value).toString(), xsd.getPrefix() + ":int");
		} else if (value instanceof Double) {
			/*
			 * Simple key-value map <item><key
			 * xsi:type="xsd:string">name</key><value
			 * xsi:type="xsd:float">value</value></item>
			 */
			return this.typedElement(elementNs, name,
					((Double) value).toString(), xsd.getPrefix() + ":float");
		} else if (value instanceof String[]) {
			/*
			 * String Array is represented by a list of items <item
			 * SOAP-ENC:arrayType="xsd:string[length]"
			 * xsi:type="SOAP-ENC:Array"> <item
			 * xsi:type="xsd:string">string</item> <!-- more items if array
			 * contains more entries --> </item>
			 */
			String[] stringArray = (String[]) value;
			OMElement arrayArg = fac.createOMElement(name, elementNs);
			arrayArg.addAttribute("arrayType", xsd.getPrefix() + ":string["
					+ stringArray.length + "]", soapEnc);
			arrayArg.addAttribute("type", soapEnc.getPrefix() + ":Array", xsi);
			for (String item : stringArray) {
				arrayArg.addChild(typedElement(elementNs, "item", item));
			}
			return arrayArg;
		} else if (value instanceof List) {
			List<Object> list = (List<Object>) value;
			OMElement arrayArg = fac.createOMElement(name, elementNs);
			arrayArg.addAttribute("arrayType", xsd.getPrefix() + ":ur-type["
					+ list.size() + "]", soapEnc);
			arrayArg.addAttribute("type", soapEnc.getPrefix() + ":Array", xsi);
			for (Object item : list) {
				arrayArg.addChild(typedElement(elementNs, "item", item));
			}
			return arrayArg;
		} else if (value instanceof Map) {
			/*
			 * Map is represented by a list of key-value pairs <item
			 * xsi:type="SOAP-XML:Map"> <item><key
			 * xsi:type="xsd:string">name-of-key</key><value
			 * xsi:type="xsd:XX">value</value></item> <!-- more items if map
			 * contains more entries--> </item>
			 */
			Map<String, Object> argMap = (Map<String, Object>) value;
			OMElement mapArg = fac.createOMElement(name, elementNs);
			mapArg.addAttribute("type", soapXml.getPrefix() + ":Map", xsi);
			for (String key : argMap.keySet()) {
				mapArg.addChild(keyValue(key, argMap.get(key)));
			}
			return mapArg;
		}
		throw new RuntimeException("keyValue not implemented for "
				+ value.getClass().toString());
	}

	/**
	 * Create a simple element with a xsi:type attribute
	 * 
	 * @param elementNs
	 * @param name
	 * @param value
	 * @param valueType
	 * @return
	 */
	private OMElement typedElement(OMNamespace elementNs, String name,
			String value, String valueType) {
		OMElement element = fac.createOMElement(name, elementNs);
		element.addAttribute("type", valueType, xsi);
		element.addChild(fac.createOMText(element, value));
		return element;
	}

	/**
	 * Creates an key-value elements contained by an item-element
	 * 
	 * @param key
	 * @param value
	 * @return
	 */
	private OMElement keyValue(String key, Object value) {
		OMElement item = fac.createOMElement("item", noNs);
		item.addChild(typedElement(noNs, "key", key));
		item.addChild(typedElement(noNs, "value", value));
		return item;
	}
}
